Spring Boot之统一分页

采用MVC结构的Java Web Application免不了遇到很普遍的分页问题,我们可以用一些小技巧来统一处理。

场景

前端请求时,对于列表数据,需要展示当前页编号、总页数、页大小、对象总个数等信息,因此需要后端对从DB获取的数据进行包装。

DAO

以MyBatis为例,从DB读取的列表数据通常是List,且可手动指定页数和页大小。

1
2
3
4
5
6
@Select("SELECT * FROM t_user WHERE enable_flag = 1 LIMIT #{offset}, #{size}")
@Results({
@Result(property = "id", column = "id"),
@Result(property = "name", column = "name")
})
List<User> getPaged(@Param("offset") Integer offset, @Param("size") Integer size);

包装List

我们需要对List进行统一的包装处理,定义一个泛型类PagedList

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class PagedList <T> {
private List<T> list;
private int total;
private int pageNumber;
private int pageSize;
private int totalPage;

public PagedList(List<T> list, int total, int pageNumber, int pageSize) {
this.list = list;
this.total = total;
this.pageNumber = pageNumber;
this.pageSize = pageSize;
this.totalPage = total/pageSize;
}

public static <T> PagedList<T> of(List<T> list, int total) {
return new PagedList<>(list, total);
}

public static <T> PagedList<T> of(List<T> list, int total, int pageNumber, int pageSize) {
return new PagedList<>(list, total, pageNumber, pageSize);
}

public static <T> PagedList empty() {
return null;
}

public <R> PagedList<R> map(Function<T, R> func) {
if(null == list) {
return empty();
}
return PagedList.of(list.stream().map(func).collect(Collectors.toList()), total);
}

//getters & setters
...
}

从DB中获取的列表数据存放在List<T> list中,且从构造方法的参数中即可得到当前页编号、总页数、页大小、对象总个数等。而map方法,下文会细讲。

Service

在Service层,只需额外获取对象总数即可构造出PagedList对象

1
2
3
4
5
public PagedList<User> getAll(Integer offset, Integer size) {
List<User> users = userDAO.getPaged(offset, size);
int total = userMapper.countAll();
return PagedList.of(users, total);
}

Controller

在Controller层,通常直接返回该PagedList即可。

1
2
3
4
5
6
@RequestMapping(method = RequestMethod.GET)
public PagedList<User> getRoles(
@RequestParam(name = "pageNumber", required = false) Integer offset,

@RequestParam(name = "pageSize", required = false) Integer size) {

return userService.getAll(offset, size);
}

Converter

但通常我们不会直接返回从DB中获取的原始数据,还需要对其进一步处理,如将上述获取的用户列表中用户对象的ID隐去,因此需要一个Converter
返回给前端的数据中不包含用户的ID,如下:

1
2
3
4
5
6
7
8
9
10
public class UserResponse {
@JsonProperty
private String name;
public void setName(String name){
this.name = name;
}
public String getName(){
return name;
}
}

而Converter中的方法是一个Wrapper:

1
2
3
4
5
6
7
8
9
public class Converter {
public UserResponse toResponse(User user) {
UserResponse userResponse = new UserResponse();
if(null != user){
userResponse.setName(user.getName());
}
return userResponse;
}
}

在Controller中使用lambda表达式对列表中的所有对象统一转换,而上文中提到的PagedListmap方法则对批量转换提供了支持。

1
2
3
4
5
6
7
8
9
@Autowired
private Converter converter;

@RequestMapping(method = RequestMethod.GET)
public PagedList<User> getRoles(
@RequestParam(name = "pageNumber", required = false) Integer offset,

@RequestParam(name = "pageSize", required = false) Integer size) {

return userService.getAll(offset, size).map(converter::toResponse);
}

1
2
3
4
5
6
7
8
9
public <R> PagedList<R> map(Function<T, R> func) {
if(null == list) {
return empty();
}
// 1. stream: 获取列表的stream
// 2. map: 对列表的每个对象都执行func方法
// 3. collect: 将处理后的对象放入到一个新的list中
return PagedList.of(list.stream().map(func).collect(Collectors.toList()), total);
}